<?php
require_once dirname(__FILE__).'/../DocParsing/AIS_DocComment.php';
require_once dirname(__FILE__).'/../DocParsing/AIS_DocCommentFunction.php';
require_once dirname(__FILE__).'/../DocParsing/AIS_DocCommentTagParamWithRestrictions.php';
require_once dirname(__FILE__).'/../Validate/Interfaces/AIS_JsonValidate.php';
require_once 'AIS_ObjectTypeCompatibilityValidatorError.php';

/**
 * 
 * This class checks if an object is compatible with a certain type. An Object is compatible with a type
 * if the type is primitive and the object itself is the same primitive (type is "int" and object is a number)
 * or if the type is a class type and the object contains the same properties the class type defines. To be able
 * to check an object versus a class type, all properties of the class must be annotated with the "@var" Annotation
 * that declares the type of the Property.
 * Following primitive types are valid ones:
 * 	int
 * 	float
 * 	double
 * 	string
 * 	bool
 * Properties can also have an arbitrary class type, where each of the properties in the class type must also be annotated.
 * Also, array is possible, but you must declare what type the array contains in Brackets, for 
 * example: "array{int}" for an array that contains integers.
 * 
 * Always avoid cyclic references, as they will always result in an object not being valid.
 */
class AIS_ObjectTypeCompatibilityValidator
{
	/**
	 * @var array
	 */
	private $primiviteTypes = array(
	"int"=>"is_numeric",
	"bool"=>"is_bool",
	"float"=>"is_numeric",
	"double"=>"is_numeric",
	"string"=>"is_string",
	"mixed"=>"");
	
	/**
	 * Sets the error message if an object was not compatible with a type
	 * 
	 * @param string $type
	 * @param string $object
	 * @param string $error
	 * @param int $code
	 */
	private function setErrorMessage($type, $object, $error, $code)
	{
		$this->errorMessage = new AIS_ObjectTypeCompatibilityValidatorError();
		$this->errorMessage->message = $error;
		$this->errorMessage->expectedType = $type;
		$this->errorMessage->receivedValue = $object;
		$this->errorMessage->errorCode = $code;
	}
	/**
	 * Checks if the given Array is compatible with the given type.
	 * 
	 * @return bool True if object is compatible, false otherwise
	 * @param string $type String describing the type to check
	 * @param array $object 
	 */
	private function checkArrayType($type, $object)
	{
		$subtype = AIS_DocComment::getArrayType($type);
			if (!is_array($object))
			{
				$this->setErrorMessage($type, $object, "Value is not an array", 1);
				return false;
			}
			$b = true;
			$i = 0;
			foreach ($object as $el)
			{
				$b = $b && $this->validateObjectWithType($el, $subtype);
				if (!$b)
				{
					$this->errorMessage->trace = "->array($subtype)[".$i."]".$this->errorMessage->trace;
					return false;
				}
				$i++;
			}
			return $b;
	}
	/**
	 * 
	 * Checks if given object is compatible with the given Type
	 * 
	 * @return True if object is compatible, false otherwise
	 * @param string $type Type of a defined class
	 * @param mixed $object An Object that is not an array and is not a primitive type
	 */
	private function checkObjectType($type, $object)
	{
		/* @var obj ReflectionClass */
		$obj = new ReflectionClass($type);
		$prop = $obj->getProperties();
		$objVars = get_object_vars($object);
		$result = true;
		foreach ($prop as $property)
		{
			$comment = new AIS_DocComment($property->getDocComment());
			$tags = $comment->getTags("@var");
			$tag = $tags[0];
			$name = $property->getName();
			if (!array_key_exists($name, $objVars))
			{
				$this->setErrorMessage($type, $object, "Member in Object does not exist", 2);
				$this->errorMessage->expectedMemberName = $name;
				return false;
			}
			$result = $result && $this->validateObjectWithDocCommentTag($objVars[$name], $tag);
			if (!$result)
			{
				$this->errorMessage->trace = "->$type::".$name.$this->errorMessage->trace;
				return false;
			}
		}
		if ($obj->implementsInterface('AIS_JsonValidate'))
		{
			/* @var method ReflectionMethod */
			$method = $obj->getMethod("isValid");
			$b =  $method->invokeArgs(NULL,array($object));
			if (!$b)
			{
				$this->setErrorMessage($type, $object, "Object Members have invalid values.", 1);
			}
			return $b;
		}
		return $result;
	}
	
	private function checkPrimitiveType($type, $object)
	{
		if ($type == "mixed")
		{
			return true;
		}
			$b = $this->primiviteTypes[$type]($object);
			if (!$b)
			{
				$this->setErrorMessage($type, $object, "Wrong primitive value type",1);
			}
			return $b;
		
	}
	
	private $primitiveTypeChecks = array(
	"int"=>"isValidNumber",
	"float"=>"isValidNumber",
	"double"=>"isValidNumber",
	"string"=>"isValidString");
	/**
	 * Enter description here...
	 *
	 * @param mixed $object
	 * @param AIS_DocCommentTagParamWithRestrictions $tag
	 * @return bool
	 */
	public function validateObjectWithDocCommentTag(&$object, $tag)
	{
		if ($object == null)
		{
			if ($tag->isOptional())
			{
				return true;
			}
			if ($tag->hasDefault())
			{
				$object = $tag->getDefault();
				return true;
			}
		}
		$type = $tag->getValue(0);
		$res = $this->validateObjectWithType($object, $type);
		if ($res)
		{
			$func = $this->primitiveTypeChecks[$type];
			if ($func)
			{
				$b = $tag->$func($object);
				if (!$b)
				{
					$this->setErrorMessage($type, $object, "Primitive type has an illegal value.", 1);
				}
				return $b;
			}
			if (AIS_DocComment::isArray($type))
			{
				$b = $tag->isValidArray($object);
				if (!$b)
				{
					$this->setErrorMessage($type, $object, "Array has illegal size.",3);
				}
				return $b;
			}
		}
		return $res;
	}
	
	/**
	 * Checks if the given Object is compatible with the given type.
	 *
	 * @param mixed $object an arbitrary Object
	 * @param string $type The tag which is used to check the object
	 * @return bool True if object is compatible, false otherwise
	 */
	public function validateObjectWithType($object, $type)
	{
		if ($type == NULL)
		{
			throw new Exception("Internal Error");
		}
		if (array_key_exists($type, $this->primiviteTypes))
		{
			return $this->checkPrimitiveType($type, $object);
		}
		if (AIS_DocComment::isArray($type))
		{
			return $this->checkArrayType($type, $object);
		}
		if (!is_object($object))
		{
			$this->setErrorMessage($type, $object, "Value is not an object", 1);
			return false;
		}
		return $this->checkObjectType($type, $object);
		
	}
	/**
	 * Returns an error message if an error happened
	 *
	 * @return AIS_ObjectTypeCompatibilityValidatorError
	 */
	public function getErrorMessage()
	{
		return $this->errorMessage;
	}
	/**
	 * The error message if an error happened
	 *
	 * @var AIS_ObjectTypeCompatibilityValidatorError
	 */
	private $errorMessage;
}
?>